﻿// -----------------------------------------------------------------------
// <copyright file="ProxyClient.cs" company="TaskyMedia LLC">
//  Copyright (c) TaskyMedia LLC
// </copyright>
// -----------------------------------------------------------------------

namespace SohpClient
{
    using System;
    using System.Globalization;
    using System.IO;
    using System.Net;

    /// <summary>
    /// A client class used to establish and maintain connections to a SOHP server.
    /// </summary>
    public class ProxyClient
    {
        #region Constants

        /// <summary>
        /// The version of the SOHP protocol implemented by the server.
        /// </summary>
        public const string ProtocolVersion = "1.0";

        #endregion

        #region Fields

        /// <summary>
        /// The SOHP-server host URI base (e.g. http://sohp.domain.com/)
        /// </summary>
        private string serverUriBase;

        /// <summary>
        /// Contains the last known state of whether the client is connected.
        /// </summary>
        private bool isConnected;

        /// <summary>
        /// The connection identifier assigned by the SOHP server.
        /// </summary>
        private int connectionId;        

        /// <summary>
        /// The connection secret used to generate request signatures.
        /// </summary>
        private long connectionSecret;

        /// <summary>
        /// The time difference between the SOHP server and the client.
        /// </summary>
        private long serverTimestampAdjust;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ProxyClient"/> class.
        /// </summary>
        /// <param name="serverUriBase">The SOHP host server URI base.</param>
        public ProxyClient(Uri serverUriBase)
        {
            if (serverUriBase == null)
            {
                throw new ArgumentNullException("serverUriBase", "Server URI base cannot be null");
            }

            this.serverUriBase = serverUriBase.AbsolutePath;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="ProxyClient"/> class.
        /// </summary>
        /// <param name="serverUriBase">The SOHP host server URI base.</param>
        public ProxyClient(string serverUriBase)
            : this(new Uri(serverUriBase))
        {   
        }

        #endregion

        #region Events

        /// <summary>
        /// Occurs when an asynchronous request to connect has completed.
        /// </summary>
        public event EventHandler<AsyncCompleteEventArgs> ConnectComplete;

        /// <summary>
        /// Occurs when an asynchronous request to send data has completed.
        /// </summary>
        public event EventHandler<AsyncCompleteEventArgs> SendComplete;

        /// <summary>
        /// Occurs when an asynchronous request to receive data has completed.
        /// </summary>
        public event EventHandler<ReceiveCompleteEventArgs> ReceiveComplete;

        #endregion

        #region Properties

        /// <summary>
        /// Gets a value indicating whether the client is connected.
        /// </summary>
        /// <value>
        ///     <c>true</c> if the client is connected; otherwise, <c>false</c>.
        /// </value>
        public bool IsConnected
        {
            get
            {
                return this.isConnected;
            }
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Begins an asynchronous operation to connect to the specified host.
        /// </summary>
        /// <param name="hostName">The host name to which the client should connect.</param>
        /// <param name="port">The port to be used in the connection.</param>
        /// <param name="state">A custom state object.</param>
        public void ConnectAsync(string hostName, int port, object state)
        {
            HttpWebRequest httpReq = HttpWebHelper.GetOpenRequest(this.serverUriBase, hostName, port, ProxyClient.ProtocolVersion);
            httpReq.Method = "POST";
            WebRequestState stateContainer = new WebRequestState(httpReq, state);
            httpReq.BeginGetResponse(this.ConnectCallback, stateContainer);
        }

        /// <summary>
        /// Begins an asynchronous operation to send data to the server.
        /// </summary>
        /// <param name="dataToSend">The data to send.</param>
        /// <param name="state">A custom state object.</param>
        public void SendAsync(byte[] dataToSend, object state)
        {
            HttpWebRequest httpReq = HttpWebHelper.GetSendRequest(this.serverUriBase, ProxyClient.ProtocolVersion);
            httpReq.Method = "POST";
            this.SetupAuthRequest(ref httpReq, "SEND");

            SendDataWebRequestState stateContainer = new SendDataWebRequestState(httpReq, dataToSend, state);
            httpReq.BeginGetRequestStream(this.SendCallback, stateContainer);
        }

        /// <summary>
        /// Begins an asynchronous operation to receive data from the server.
        /// </summary>
        /// <param name="state">A custom state object.</param>
        public void ReceiveAsync(object state)
        {
            HttpWebRequest httpReq = HttpWebHelper.GetReceiveRequest(this.serverUriBase, ProxyClient.ProtocolVersion);
            httpReq.Method = "POST";
            this.SetupAuthRequest(ref httpReq, "RECEIVE");

            WebRequestState stateContainer = new WebRequestState(httpReq, state);
            httpReq.BeginGetResponse(this.ReceiveCallback, stateContainer);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Handles the asynchronous connect request callback.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        private void ConnectCallback(IAsyncResult asyncResult)
        {
            WebRequestState stateContainer = asyncResult.AsyncState as WebRequestState;
            HttpWebRequest httpReq = stateContainer.Request;
            AsyncCompleteEventArgs resultArgs;

            try
            {
                HttpWebResponse httpResp = (HttpWebResponse)httpReq.EndGetResponse(asyncResult);

                if (httpResp.StatusCode == HttpStatusCode.OK)
                {
                    this.ConnectSuccess(httpResp);
                    resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, true);
                }
                else
                {
                    resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, false, new WebException(string.Format(CultureInfo.InvariantCulture, "Response from SOHP Server: {0}", httpResp.StatusCode)));
                    this.isConnected = false;
                }

                httpResp.Close();
            }
            catch (WebException)
            {
                resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, false);
                this.isConnected = false;
            }

            if (this.ConnectComplete != null)
            {
                this.ConnectComplete(this, resultArgs);
            }
        }

        /// <summary>
        /// Handles the asynchronous send callback to open the output stream.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        private void SendCallback(IAsyncResult asyncResult)
        {
            SendDataWebRequestState stateContainer = asyncResult.AsyncState as SendDataWebRequestState;

            if (stateContainer != null)
            {
                HttpWebRequest httpReq = stateContainer.Request;

                using (Stream outputStream = httpReq.EndGetRequestStream(asyncResult))
                {
                    outputStream.Write(stateContainer.GetData(), 0, stateContainer.DataLength);
                }

                // Get the response
                WebRequestState responseState = new WebRequestState(httpReq, stateContainer.AsyncState);
                httpReq.BeginGetResponse(this.SendResponseCallback, responseState);
            }
        }

        /// <summary>
        /// Handles the asynchronous receive request callback.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        private void ReceiveCallback(IAsyncResult asyncResult)
        {
            WebRequestState stateContainer = asyncResult.AsyncState as WebRequestState;
            HttpWebRequest httpReq = stateContainer.Request;
            ReceiveCompleteEventArgs resultArgs;

            try
            {
                HttpWebResponse httpResp = (HttpWebResponse)httpReq.EndGetResponse(asyncResult);

                if (httpResp.StatusCode == HttpStatusCode.OK)
                {
                    if (httpResp.ContentLength > 0)
                    {
                        byte[] data = new byte[httpResp.ContentLength];
                        using (Stream s = httpResp.GetResponseStream())
                        {
                            s.Read(data, 0, data.Length);
                        }

                        resultArgs = new ReceiveCompleteEventArgs(data, stateContainer.AsyncState, true);
                    }
                    else
                    {
                        resultArgs = new ReceiveCompleteEventArgs(null, stateContainer.AsyncState, true);
                    }
                }
                else
                {
                    resultArgs = new ReceiveCompleteEventArgs(null, stateContainer.AsyncState, false);
                }

                httpResp.Close();
            }
            catch (WebException)
            {
                resultArgs = new ReceiveCompleteEventArgs(null, stateContainer.AsyncState, false);
            }

            if (this.ReceiveComplete != null)
            {
                this.ReceiveComplete(this, resultArgs);
            }
        }

        /// <summary>
        /// Handles the asynchronous send response callback.
        /// </summary>
        /// <param name="asyncResult">The async result.</param>
        private void SendResponseCallback(IAsyncResult asyncResult)
        {
            HttpWebResponse httpResp;
            this.HandleResponse(asyncResult, this.SendComplete, out httpResp);
        }

        /// <summary>
        /// Handles the transformation of the client upon a successful connect response from the server.
        /// </summary>
        /// <param name="httpResp">The <see cref="HttpWebResponse"/> object from the request.</param>
        private void ConnectSuccess(HttpWebResponse httpResp)
        {
            long serverTime = long.Parse(httpResp.Headers["X-SOHP-ATS"], CultureInfo.InvariantCulture);
            long clientTime = DateTime.UtcNow.Ticks;
            this.serverTimestampAdjust = clientTime - serverTime;
            this.connectionSecret = long.Parse(httpResp.Headers["X-SOHP-ConnectionSecret"], CultureInfo.InvariantCulture);
            this.connectionId = int.Parse(httpResp.Headers["X-SOHP-ConnectionID"], CultureInfo.InvariantCulture);

            // We're "connected" until told otherwise
            this.isConnected = true;
        }

        /// <summary>
        /// Handles an asynchronous operation completion for the <see cref="HttpWebRequest"/> <c>BeginGetResponse</c> method. Inspects
        /// the HTTP response code to determine if the action was successful and, if so, invokes the event specified in <paramref name="targetEvent"/>.
        /// </summary>
        /// <param name="asyncResult">The <see cref="IAsyncResult"/> instance from the callback.</param>
        /// <param name="targetEvent">The event to be invoked upon a successful response.</param>
        /// <param name="httpResp">The <see cref="HttpWebResponse"/> object associated with the request.</param>
        /// <returns>Returns a <c>bool</c> indicating whether the action was successful.</returns>
        private bool HandleResponse(IAsyncResult asyncResult, EventHandler<AsyncCompleteEventArgs> targetEvent, out HttpWebResponse httpResp)
        {
            bool result = false;
            WebRequestState stateContainer = asyncResult.AsyncState as WebRequestState;
            HttpWebRequest httpReq = stateContainer.Request;
            AsyncCompleteEventArgs resultArgs;

            try
            {
                httpResp = (HttpWebResponse)httpReq.EndGetResponse(asyncResult);

                if (httpResp.StatusCode == HttpStatusCode.OK)
                {
                    result = true;
                    resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, true);
                }
                else
                {
                    resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, false, new WebException(string.Format(CultureInfo.InvariantCulture, "Response from SOHP Server: {0}", httpResp.StatusCode)));
                    this.isConnected = false;
                }

                httpResp.Close();
            }
            catch (WebException)
            {
                httpResp = null;
                resultArgs = new AsyncCompleteEventArgs(stateContainer.AsyncState, false);
                this.isConnected = false;
            }

            if (targetEvent != null)
            {
                targetEvent(this, resultArgs);
            }

            return result;
        }

        /// <summary>
        /// Sets up the required parameters for an Authenticated request.
        /// </summary>
        /// <param name="httpReq">The <see cref="HttpWebRequest"/> object to set up.</param>
        /// <param name="operation">The operation name.</param>
        private void SetupAuthRequest(ref HttpWebRequest httpReq, string operation)
        {
            long adjustedTime = DateTime.UtcNow.Ticks - this.serverTimestampAdjust;
            string signature = SohpShared.RequestSigning.GenerateSignature(operation, adjustedTime, this.connectionSecret);

            httpReq.Headers["X-SOHP-ConnectionID"] = this.connectionId.ToString(CultureInfo.InvariantCulture);
            httpReq.Headers["X-SOHP-ATS"] = adjustedTime.ToString(CultureInfo.InvariantCulture);
            httpReq.Headers["X-SOHP-RequestSignature"] = signature;
        }

        #endregion
    }
}
